<!-- 仅适用于数据统计下方的表格，非通用型虚拟表格组件 -->
<template>
  <div
    class="simple-virtual-table"
    :style="{ height: `${height < 30 ? 30 : height}px` }"
  >
    <recycle-scroller
      class="scroller"
      :style="{ width: `${wrapperWidth}px` }"
      :items="dataSources"
      :item-size="32"
      :key-field="keyField"
      :emit-update="true"
      :detect-hover="false"
      @update="onUpdate"
    >
      <div
        slot="before"
        class="table-header"
        :style="{ width: `${totalWidth}px` }"
      >
        <!-- 虚拟滚动前添加的内容 -->
        <slot name="headerBefore"></slot>
        <table-column
          v-for="(column, index) in dataColumn"
          :key="column[columnKey]"
          :class="{
            'header-item': true,
            'fixed-left': column.fixed === 'left',
            'fixed-right': column.fixed === 'right',
          }"
          :width="columnWidthMap[column[columnKey]]"
          :default-width="defaultWidth"
          @click="(event) => onTableHeaderClick(column, event)"
          @contextmenu.native="(event) => onContextMenu(column, event)"
        >
          <slot name="header" v-bind="{ column: column, index: index }"></slot>
          <vue-draggable-resizable
            :key="column[columnKey]"
            class="table-draggable-handle"
            :data-key="`drag-handle-${column[columnKey]}`"
            :w="10"
            :x="columnWidthMap[column[columnKey]] || column.width"
            :z="1"
            axis="x"
            :draggable="true"
            :resizable="false"
            :on-drag-start="onDragStart"
            @dragging="(x, ...p) => onDrag(x, column, p)"
            @dragstop="onDragstop(column)"
          ></vue-draggable-resizable>
        </table-column>
      </div>

      <template #default="{ item, index, active }">
        <!-- 表内容区域 -->
        <div
          class="list-item-infinite"
          :class="{
            'list-item-infinite': true,
            [`tr-item-${index % 2 === 0 ? 'even' : 'odd'}`]: true,
            'select-tr': selectedRow.has(item[keyField]),
          }"
          :style="{ width: `${totalWidth}px` }"
        >
          <slot name="rowBefore" v-bind="{ row: item, index, active }"></slot>
          <div
            v-for="(column, columnIndex) in dataColumn"
            :key="column[columnKey]"
            :width="columnWidthMap[column[columnKey]]"
            :class="{
              'table-cell': true,
              'fixed-left': columnFixedMap[column[columnKey]] === 'left',
              'fixed-right': columnFixedMap[column[columnKey]] === 'right',
            }"
            :style="{
              width: `${columnWidthMap[column[columnKey]] || defaultWidth}px`,
              'flex-basis': `${
                columnWidthMap[column[columnKey]] || defaultWidth
              }px`,
            }"
            @click="(event) => onCellClick(item, column, event)"
          >
            <slot
              name="cell"
              v-bind="{ column, row: item, index, active, columnIndex }"
            ></slot>
          </div>
        </div>
      </template>
    </recycle-scroller>

    <div class="recycle-divider" :style="dragDividerStyle"></div>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator'
// @ts-ignore
// import { RecycleScroller } from 'vue-virtual-scroller'
import { RecycleScroller } from '@akryuminfinitum/vue-virtual-scroller' // 是基于vue-virtual-scroller组件并增加了一些修改：屏蔽mouseenter、mouseleave事件
// @ts-ignore
import VueDraggableResizable from 'vue-draggable-resizable'
import TableColumn from '@/components/simple-virtual-table/TableColumn.vue'
// @ts-ignore
// eslint-disable-next-line import/no-unresolved
// import RecycleScroller from '../../../node_modules/vue-virtual-scroller/src/components/RecycleScroller.vue'
import '@akryuminfinitum/vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { isNumber } from 'lodash'
import Big from 'big.js'

interface KeyValueStore {
  [key: string]: any
}

const DEFAULT_WIDTH = 150

@Component({
  components: {
    RecycleScroller,
    TableColumn,
    VueDraggableResizable,
  },
})
export default class SimpleVirtualTable extends Vue {
  @Prop({ default: '' }) columnKey!: string //
  @Prop({ default: '' }) keyField!: string // 每行数据中的唯一标识
  @Prop({ default: [] }) dataSources!: KeyValueStore[] // 详细数据
  @Prop({ default: [] }) dataColumn!: Object[] // 数据列
  @Prop({ default: 300 }) height!: number // 高度
  @Prop({ default: 0 }) headerExtraWidth!: number // 当slot#rowBefore有内容时，需传入告诉额外占的宽度，便于计算totalWidth

  private defaultWidth = DEFAULT_WIDTH // 默认列宽度
  private dragDividerLeft: number = 0 // 调整列宽时辅助线位置
  private columnWidthMap: KeyValueStore = {} // 列宽度记录
  private columnFixedMap: KeyValueStore = {} // 列固定（fixed）记录
  private totalWidth: number = 0 // 表格总宽度
  private selectedRow: Set<string> = new Set() // 选定的行数据
  private elWidth: number = 0 // 组件可视区域占的宽度

  mounted() {
    this.onDataColumnChange()
    this.elWidth = this.$el.clientWidth
    window.addEventListener('resize', this.onWindowResize)
  }

  @Watch('dataColumn')
  private onDataColumnChange() {
    const data = { ...this.columnWidthMap }
    const { width: containerWidth } = this.$el.getBoundingClientRect()
    let totalWidth = 0
    if (this.dataColumn.length === 0) {
      this.totalWidth = containerWidth
      return
    }
    const calcWidth: number =
      (containerWidth - this.headerExtraWidth) / this.dataColumn.length // 计算字段每一列可以占用的宽度
    // 计算的可占用宽度大于default宽度时，采用计算宽度
    const columnWidth =
      calcWidth > this.defaultWidth ? calcWidth : this.defaultWidth
    this.dataColumn.forEach((column: any) => {
      const width = column.width || columnWidth
      column.width = width
      data[column[this.columnKey]] = width
      totalWidth += width
      if (column.fixed) {
        this.columnFixedMap[column[this.columnKey]] = column.fixed
      }
    })
    this.columnWidthMap = { ...data }
    this.totalWidth = totalWidth + this.headerExtraWidth

    // 修改format
    this.dataColumn.forEach((column: any) => {
      const { name, numberFormat } = column
      if (numberFormat !== null && numberFormat !== undefined) {
        const { action, digit } = numberFormat
        this.dataSources.forEach((cellData: any) => {
          cellData[name] = cellData[name]
            ? this.getFormatValue(Big(cellData[name]).toNumber(), action, digit)
            : cellData[name]
        })
      }
    })
  }

  // 数字格式化
  private getFormatValue(value: any, type: string, digit: number) {
    if (typeof value === 'number') {
      if (type === '%') {
        // 百分比计数法
        value = `${Big(value).times(100).toFixed(digit)}%`
      } else if (type === 'E-NOTATION') {
        // 科学计数法
        value = `${Big(value).toExponential(digit)}`
      }
    }
    return value
  }

  /**
   * 列宽度拖拽时，辅助线的样式
   */
  private get dragDividerStyle() {
    return {
      display: this.dragDividerLeft > 0 ? 'block' : 'none',
      transform: `translateX(${this.dragDividerLeft}px)`,
    }
  }

  /**
   * 页面可视区域宽度
   */
  private get wrapperWidth() {
    if (this.totalWidth > this.elWidth) {
      // 总宽度大于可视区域，以可视区域为准
      return this.elWidth
    }
    return this.totalWidth
  }

  /**
   * 列宽度调整拖拽开始
   */
  private onDragStart(event: MouseEvent) {
    const { pageX } = event
    const { left } = this.$el.getBoundingClientRect()

    this.dragDividerLeft = pageX - left
  }

  /**
   * 列宽度调整拖拽中
   */
  private onDrag(x: number, column: any) {
    const minWidth = 5 + 21 // 21 为table-cell左右padding + border的距离，5为最小宽度
    const newWidth = Math.max(x, minWidth) // 21 为table-cell左右padding + border的距离
    const distance = x - column.width // 从拖拽开始移动的距离差
    column.width = newWidth

    if (x > minWidth) {
      this.dragDividerLeft += distance
    }
  }

  /**
   * 列宽度调整拖拽结束
   */
  private onDragstop(column: any) {
    const distance = column.width - this.columnWidthMap[column[this.columnKey]]
    this.totalWidth += distance
    this.columnWidthMap[column[this.columnKey]] = column.width
    this.dragDividerLeft = 0

    const target = this.$el.querySelector(
      `[data-key="drag-handle-${column[this.columnKey]}"]`
    )
    if (target) {
      // vue-draggable-resizable组件bug，拖动结束后，重新赋值下位置
      ;(
        target as HTMLElement
      ).style.transform = `translate(${column.width}px, 0px)`
    }
    this.$emit('column-resize', column, column.width)
  }

  /**
   * 单元格点击事件
   */
  private onCellClick(row: any, column: any, event: MouseEvent) {
    this.$emit('cell-click', row, column, event.currentTarget, event)
  }

  /**
   * 表头右击事件
   */
  private onContextMenu(column: any, event: MouseEvent) {
    this.$emit('context-menu', column, event)
  }

  /**
   * 表头点击事件
   */
  private onTableHeaderClick(column: any, event: MouseEvent) {
    this.$emit('header-click', column, event)
  }

  /**
   * scroll组件可视区域变化时触发，用来判断是否滚动到底部
   */
  private onUpdate(startIndex: number, endIndex: number) {
    this.$emit('table-body-scroll', {
      scrollToBottom: endIndex === this.dataSources.length,
    })
  }

  /**
   * 切换表格的选中态
   */
  public toggleRowSelection(
    selectData: Array<{ row: KeyValueStore; selected?: boolean }>
  ) {
    if (selectData.length === 0) {
      return
    }
    const selected: Set<string> = new Set()
    selectData.forEach((data) => {
      const key = data.row[this.keyField] // 每行数据的唯一标识
      selected.add(key)
      // if (data.selected === true || !selected.has(key)) {
      //   selected.add(key)
      // } else {
      //   selected.delete(key)
      // }
    })
    this.selectedRow = selected
  }

  /**
   * 更新元素宽度
   */
  public updateElWidth() {
    this.elWidth = this.$el.clientWidth
  }

  /**
   * 清除选中态
   */
  public clearSelection() {
    this.selectedRow = new Set()
  }

  /**
   * resize 大小
   */
  public resize() {
    this.onWindowResize()
  }

  /**
   * pagingScrollTopLeft
   */
  public pagingScrollTopLeft(x?: number, y?: number) {
    const target = this.$el.querySelector(
      '.vue-recycle-scroller.scroller'
    ) as HTMLElement
    if (!target) {
      return
    }
    if (isNumber(x)) {
      target.scrollLeft = x
    }
    if (isNumber(y)) {
      target.scrollTop = y + 50 // 制造一定的滚动差来触发vue-virtual-scroller组件scroll事件进行重新绘制
      this.$nextTick(() => {
        target.scrollTop = y
      })
    }
  }

  private onWindowResize() {
    window.requestAnimationFrame(() => {
      this.elWidth = this.$el.clientWidth
    })
  }
}
</script>

<style lang="less">
.table-cell {
  border-bottom: 1px solid #ebeef5;
  border-right: 1px solid #ebeef5;
  flex: 0 0 180px;
  // padding: 6px 10px;
  &:first-child {
    border-left: 1px solid #ebeef5;
  }

  .content {
    height: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  &.fixed-left {
    left: 0;
    position: sticky !important;
    z-index: 9;
  }

  &.fixed-right {
    position: sticky !important;
    right: 0;
    z-index: 9;
  }
}

.simple-virtual-table {
  font-size: 12px;
  height: 100%;
  overflow-y: auto;
  position: relative;

  .scroller {
    height: 100%;
    overflow-y: auto;

    &::-webkit-scrollbar {
      height: 6px;
      width: 6px;
    }

    &::-webkit-scrollbar-thumb {
      background: rgba(144, 147, 153, 0.5);
      border-radius: 6px;
    }

    &::-webkit-scrollbar-track {
      background: transparent;
      border-radius: 5px;
    }
  }

  .table-header {
    align-items: center;
    display: flex;
  }

  .list-item-infinite {
    align-items: center;
    background-color: #fafafc;
    border-bottom: 1px solid;
    border-color: #e9e9e9;
    display: flex;
    height: 36px;

    &.tr-item-odd {
      background-color: #f3f3f3;

      .table-cell {
        background-color: #f3f3f3;
      }
    }

    .table-cell {
      background-color: #fafafc;
      border-right: 1px solid #ebeef5;
      flex: 0 0 180px;
      height: 100%;
      position: relative;

      .cell-highlight {
        left: 0;
        position: absolute;
        top: 0;
        width: 100%;
      }
    }
  }

  .header-item {
    background-color: #fff;
    position: relative;

    .table-draggable-handle {
      bottom: 0;
      cursor: col-resize;
      height: 100% !important;
      left: -5px;
      position: absolute;
      touch-action: none;
    }
  }

  .user {
    align-items: center;
    display: flex;
    height: 32%;
    padding: 0 12px;
  }

  .recycle-divider {
    border-left: 1px solid #ebeef5;
    bottom: 0;
    left: 0;
    position: absolute;
    top: 0;
    width: 0;
    z-index: 10;
  }

  .vue-recycle-scroller__slot {
    position: sticky;
    top: 0;
    z-index: 9;
  }

  .vue-recycle-scroller.direction-vertical:not(.page-mode) {
    overflow-y: inherit;
  }

  .vue-recycle-scroller__item-wrapper {
    overflow: initial;
  }
}
</style>
